Skip to content

feat(nextjs): Isolate nonce fetch in Suspense boundary for PPR support#7773

Open
jacekradko wants to merge 11 commits intomainfrom
jacek/user-4607-do-not-await-nonce-in-the-clerkprovider
Open

feat(nextjs): Isolate nonce fetch in Suspense boundary for PPR support#7773
jacekradko wants to merge 11 commits intomainfrom
jacek/user-4607-do-not-await-nonce-in-the-clerkprovider

Conversation

@jacekradko
Copy link
Member

@jacekradko jacekradko commented Feb 5, 2026

Summary

  • Move nonce fetching from the server ClerkProvider's main body into a separate DynamicClerkScripts server component wrapped in Suspense
  • This allows pages using dynamic=true to remain statically renderable and compatible with PPR/cacheComponents

Problem

In the Next.js App Router server ClerkProvider, we await nonce from headers via getNonceHeaders(). This calls headers() which opts the entire page out of static rendering and breaks PPR/cacheComponents.

Solution

Isolate the nonce fetch in a Suspense boundary:

  • Create DynamicClerkScripts async server component that fetches nonce and renders scripts
  • Add getNonce cached function to utils
  • When dynamic=true, render <Suspense><DynamicClerkScripts/></Suspense>
  • Skip client ClerkScripts when server scripts are used via __internal_skipScripts prop

Test plan

  • Verify build passes
  • Test with dynamic=true - scripts should render correctly
  • Test PPR behavior - page should remain statically renderable with dynamic scripts isolated
  • Test CSP nonce functionality still works

Closes USER-4607

Summary by CodeRabbit

  • New Features

    • Dynamic server-side script loading (Suspense-wrapped) with optional UI prefetch for faster startup.
    • New optional flag to skip injecting client scripts when desired.
  • Refactor

    • Per-request nonce retrieval with caching to improve script loading under strict Content-Security-Policy and startup reliability.

Move nonce fetching from the server ClerkProvider's main body into a
separate DynamicClerkScripts server component wrapped in Suspense.
This allows pages using dynamic=true to remain statically renderable
and compatible with PPR/cacheComponents.

- Create DynamicClerkScripts async server component
- Add getNonce cached function to utils
- Skip client ClerkScripts when server scripts are used
- Pass __internal_skipScripts through KeylessProvider
@vercel
Copy link

vercel bot commented Feb 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
clerk-js-sandbox Skipped Skipped Feb 6, 2026 8:49pm

Request Review

@changeset-bot
Copy link

changeset-bot bot commented Feb 5, 2026

🦋 Changeset detected

Latest commit: 137f9f2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@clerk/nextjs Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 5, 2026

📝 Walkthrough

Walkthrough

Adds an internal boolean prop __internal_skipScripts to NextClerkProvider props and threads it through KeylessProvider, server ClerkProvider, and client ClerkProvider to optionally skip injecting client scripts. Removes direct nonce retrieval from the server ClerkProvider and introduces an async server component DynamicClerkScripts rendered inside a React Suspense boundary that fetches a cached nonce via a new exported getNonce helper. Adds a shared ClerkScriptTags component for script/preload tags and simplifies ClerkScript(s) rendering logic.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely describes the main objective: isolating nonce fetch within a Suspense boundary to support Per-Page Rendering, which is the core change across all modified files.
Linked Issues check ✅ Passed The PR fully addresses USER-4607 by moving nonce retrieval from the main ClerkProvider to a separate DynamicClerkScripts component wrapped in Suspense, preventing static rendering opts-out while maintaining CSP nonce support.
Out of Scope Changes check ✅ Passed All changes are directly scoped to preventing nonce-induced static rendering opts-out: nonce utility caching, DynamicClerkScripts component, Suspense wrapping, __internal_skipScripts flag, and ClerkScriptTags extraction are all necessary for achieving PPR compatibility.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 5, 2026

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@7773

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@7773

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@7773

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@7773

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@7773

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@7773

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@7773

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@7773

@clerk/express

npm i https://pkg.pr.new/@clerk/express@7773

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@7773

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@7773

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@7773

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@7773

@clerk/react

npm i https://pkg.pr.new/@clerk/react@7773

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@7773

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@7773

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@7773

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@7773

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@7773

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@7773

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@7773

commit: 137f9f2

@jacekradko jacekradko requested a review from Ephem February 5, 2026 18:38
…ndling

Extract duplicated script rendering into a shared ClerkScriptTags component
used by both ClerkScripts (client) and DynamicClerkScripts (server). Add
try/catch to getNonce() so errors in prerendering or "use cache" contexts
degrade gracefully instead of propagating unhandled.
…n RSC

Import clerkJSScriptUrl, buildClerkJSScriptAttributes, clerkUIScriptUrl
from @clerk/shared/loadClerkJsScript instead of @clerk/react/internal in
the shared ClerkScriptTags component. The @clerk/react/internal barrel
re-exports modules that use React.createContext, which breaks when the
RSC bundler evaluates the barrel in server component context.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/nextjs/src/app-router/server/utils.ts`:
- Around line 156-180: Add automated tests for getNonce covering three
scenarios: (1) when the request provides X-Nonce header ensure getNonce returns
that value and caching via reactCache deduplicates repeated calls; (2) when
X-Nonce is absent but Content-Security-Policy contains a script-nonce ensure
getNonce falls back to getScriptNonceFromHeader and returns the extracted nonce;
and (3) when the dynamic import throws a prerendering bailout
(isPrerenderingBailout returns true) ensure the bailout is rethrown. In tests,
mock the dynamic import of 'next/headers' to return a headers() object with
get() behavior, spy/override getScriptNonceFromHeader and isPrerenderingBailout
to simulate CSP and bailout cases, and verify caching semantics by calling
getNonce multiple times in the same test to confirm reactCache deduplication.

Comment on lines 156 to 180
/**
* Fetches the nonce from request headers.
* Uses React.cache to deduplicate calls within the same request.
*/
// React.cache is only available in RSC environments; provide a no-op fallback for tests/non-RSC contexts.
const reactCache = typeof React.cache === 'function' ? React.cache : <T extends (...args: any[]) => any>(fn: T): T => fn;

export const getNonce = reactCache(async function getNonce(): Promise<string> {
try {
// Dynamically import next/headers
// @ts-expect-error: Cannot find module 'next/headers' or its corresponding type declarations.ts(2307)
const { headers } = await import('next/headers');
const headersList = await headers();
const nonce = headersList.get('X-Nonce');
return nonce
? nonce
: // Fallback to extracting from CSP header
getScriptNonceFromHeader(headersList.get('Content-Security-Policy') || '') || '';
} catch (e) {
if (isPrerenderingBailout(e)) {
throw e;
}
// Graceful degradation — scripts load without nonce
return '';
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add automated coverage for getNonce behavior.
No tests are included for the new nonce retrieval/caching paths; please add tests covering X-Nonce presence, CSP fallback, and prerendering bailout handling.

As per coding guidelines, “If there are no tests added or modified as part of the PR, please suggest that tests be added to cover the changes.”

🤖 Prompt for AI Agents
In `@packages/nextjs/src/app-router/server/utils.ts` around lines 156 - 180, Add
automated tests for getNonce covering three scenarios: (1) when the request
provides X-Nonce header ensure getNonce returns that value and caching via
reactCache deduplicates repeated calls; (2) when X-Nonce is absent but
Content-Security-Policy contains a script-nonce ensure getNonce falls back to
getScriptNonceFromHeader and returns the extracted nonce; and (3) when the
dynamic import throws a prerendering bailout (isPrerenderingBailout returns
true) ensure the bailout is rethrown. In tests, mock the dynamic import of
'next/headers' to return a headers() object with get() behavior, spy/override
getScriptNonceFromHeader and isPrerenderingBailout to simulate CSP and bailout
cases, and verify caching semantics by calling getNonce multiple times in the
same test to confirm reactCache deduplication.

@jacekradko
Copy link
Member Author

!snapshot

@clerk-cookie
Copy link
Collaborator

Hey @jacekradko - the snapshot version command generated the following package versions:

Package Version
@clerk/agent-toolkit 0.3.0-snapshot.v20260206212043
@clerk/astro 3.0.0-snapshot.v20260206212043
@clerk/backend 3.0.0-snapshot.v20260206212043
@clerk/chrome-extension 3.0.0-snapshot.v20260206212043
@clerk/clerk-js 6.0.0-snapshot.v20260206212043
@clerk/dev-cli 1.0.0-snapshot.v20260206212043
@clerk/expo 3.0.0-snapshot.v20260206212043
@clerk/expo-passkeys 1.0.0-snapshot.v20260206212043
@clerk/express 2.0.0-snapshot.v20260206212043
@clerk/fastify 2.7.0-snapshot.v20260206212043
@clerk/localizations 4.0.0-snapshot.v20260206212043
@clerk/msw 0.0.1-snapshot.v20260206212043
@clerk/nextjs 7.0.0-snapshot.v20260206212043
@clerk/nuxt 2.0.0-snapshot.v20260206212043
@clerk/react 6.0.0-snapshot.v20260206212043
@clerk/react-router 3.0.0-snapshot.v20260206212043
@clerk/shared 4.0.0-snapshot.v20260206212043
@clerk/tanstack-react-start 1.0.0-snapshot.v20260206212043
@clerk/testing 2.0.0-snapshot.v20260206212043
@clerk/ui 1.0.0-snapshot.v20260206212043
@clerk/upgrade 2.0.0-snapshot.v20260206212043
@clerk/vue 2.0.0-snapshot.v20260206212043

Tip: Use the snippet copy button below to quickly install the required packages.
@clerk/agent-toolkit

npm i @clerk/agent-toolkit@0.3.0-snapshot.v20260206212043 --save-exact

@clerk/astro

npm i @clerk/astro@3.0.0-snapshot.v20260206212043 --save-exact

@clerk/backend

npm i @clerk/backend@3.0.0-snapshot.v20260206212043 --save-exact

@clerk/chrome-extension

npm i @clerk/chrome-extension@3.0.0-snapshot.v20260206212043 --save-exact

@clerk/clerk-js

npm i @clerk/clerk-js@6.0.0-snapshot.v20260206212043 --save-exact

@clerk/dev-cli

npm i @clerk/dev-cli@1.0.0-snapshot.v20260206212043 --save-exact

@clerk/expo

npm i @clerk/expo@3.0.0-snapshot.v20260206212043 --save-exact

@clerk/expo-passkeys

npm i @clerk/expo-passkeys@1.0.0-snapshot.v20260206212043 --save-exact

@clerk/express

npm i @clerk/express@2.0.0-snapshot.v20260206212043 --save-exact

@clerk/fastify

npm i @clerk/fastify@2.7.0-snapshot.v20260206212043 --save-exact

@clerk/localizations

npm i @clerk/localizations@4.0.0-snapshot.v20260206212043 --save-exact

@clerk/msw

npm i @clerk/msw@0.0.1-snapshot.v20260206212043 --save-exact

@clerk/nextjs

npm i @clerk/nextjs@7.0.0-snapshot.v20260206212043 --save-exact

@clerk/nuxt

npm i @clerk/nuxt@2.0.0-snapshot.v20260206212043 --save-exact

@clerk/react

npm i @clerk/react@6.0.0-snapshot.v20260206212043 --save-exact

@clerk/react-router

npm i @clerk/react-router@3.0.0-snapshot.v20260206212043 --save-exact

@clerk/shared

npm i @clerk/shared@4.0.0-snapshot.v20260206212043 --save-exact

@clerk/tanstack-react-start

npm i @clerk/tanstack-react-start@1.0.0-snapshot.v20260206212043 --save-exact

@clerk/testing

npm i @clerk/testing@2.0.0-snapshot.v20260206212043 --save-exact

@clerk/ui

npm i @clerk/ui@1.0.0-snapshot.v20260206212043 --save-exact

@clerk/upgrade

npm i @clerk/upgrade@2.0.0-snapshot.v20260206212043 --save-exact

@clerk/vue

npm i @clerk/vue@2.0.0-snapshot.v20260206212043 --save-exact

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants